package scanner;

import java.io.IOException;
import java.util.LinkedList;
import java.util.List;
import structures.table.symboltable.GlobalItemInfoTable;
import structures.table.symboltable.TerminalType;

/**
 *
 * @author Maxim
 */
public class Scanner implements ScannerInterface{

    private String output;
    private GlobalItemInfoTable table;
    private SymbolType[] syms;
    private CommentChars commChars;
    private List<Token> tokens = new LinkedList<Token>();
    public static final SymbolType[] C_SYMS = new SymbolType[]{
        SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, // 00-06
        SymbolType.WHITESPACE, // 07		TAB
        SymbolType.ERROR, SymbolType.ERROR, // 08-09
        SymbolType.WHITESPACE, // 0a		CR
        SymbolType.ERROR, SymbolType.ERROR, // 0b-0c
        SymbolType.WHITESPACE, // 0d		LF
        SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, // 0e-14
        SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, // 15-1b
        SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, SymbolType.ERROR, // 1c-1f
        SymbolType.WHITESPACE, // 20		SPACE
        SymbolType.MULTI_SYMBOL_DELIMITER, // 21		!
        SymbolType.STRING, // 22		"
        SymbolType.ERROR, SymbolType.ERROR, // 23-24
        SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, // 25-26	% &
        SymbolType.CHAR, // 27		'
        SymbolType.DELIMITER, SymbolType.DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.DELIMITER, // 28-2c	( ) * + ,
        SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, // 2d-2f	- . /
        SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, // 30-35	0-5
        SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, SymbolType.NUMBER, // 36-39	6-9
        SymbolType.DELIMITER, SymbolType.DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, // 3a-3e	: ; < = >
        SymbolType.MULTI_SYMBOL_DELIMITER, // 3f		?
        SymbolType.ERROR, // 40
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 41-46	A-F
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 47-4c	G-L
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 4d-52	M-R
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 53-58	S-X
        SymbolType.LETTER, SymbolType.LETTER, // 59-5a	Y-Z
        SymbolType.DELIMITER, // 5b		[
        SymbolType.LETTER, // 5c		\
        SymbolType.DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, // 5d-5e	] ^
        SymbolType.LETTER, // 5f		_
        SymbolType.ERROR, // 60
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 61-66	a-f
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 67-6c	g-l
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 6d-72	m-r
        SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, SymbolType.LETTER, // 73-78	s-x
        SymbolType.LETTER, SymbolType.LETTER, // 79-7a	y-z
        SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, SymbolType.MULTI_SYMBOL_DELIMITER, // 7b-7e	{ | } ~
        SymbolType.ERROR // 7f
    };

    public Scanner(GlobalItemInfoTable globalItemInfoTable, CommentChars chars) {
        table = globalItemInfoTable;
        commChars = chars;
        syms = C_SYMS;
    }

    public Scanner(GlobalItemInfoTable globalItemInfoTable, SymbolType[] SymTable) {
        syms = SymTable.clone();
    }

    @Override
    public ScannerOutput scan(String input) {

        output = "";
        tokens = new LinkedList<Token>();
        CharacterStream stream;
        
        stream = new CharacterStream(input);
        
        try {
            for (int next = stream.read(); next != -1;) // next = stream.read())
            {
                switch (syms[next]) {
                    case DELIMITER:
                    case MULTI_SYMBOL_DELIMITER:
                        next = getDelimiter(next, stream);
                        break;
                    case CHAR:
                        next = getConstChar(next, stream);
                        break;
                    case STRING:
                        next = getConstString(next, stream);
                        break;
                    case LETTER:
                        next = getIdentifier(next, stream);
                        break;
                    case NUMBER:
                        next = getConstNumber(next, stream);
                        break;
                    case ERROR:
                        return new ScannerOutput(makeError(input, stream.getRow(), stream.getCol(), "Unexpected symbol '" + ((char) next) + "'"));
                    // next = stream.read();
                    // break;
                    case WHITESPACE:
                        next = stream.read();
                }
            }
        } catch (Exception ex) {
            return new ScannerOutput(makeError(input, stream.getRow(), stream.getCol(), ex.getMessage()));
        }
        return new ScannerOutput(tokens);
    }

    private ScannerError makeError(String filename, int row, int col, String message) {
        return new ScannerError(ScannerError.SCANNING_ERROR,
                String.format("%s, %d, %d: %s\n", filename, row, col, message),
                row, col);
    }

    private void addToken(String token, TerminalType type, int row, int col) {
        tokens.add(new Token(table.addToken(token, type), row, col));
    }

    private int getDelimiter(int next, CharacterStream stream) throws IOException, Exception {
        if (next == commChars.begFirst) {
            return getComment(next, stream);
        }
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        if (syms[next] == SymbolType.DELIMITER) {
            addToken(token, TerminalType.PUNCTUATOR, row, col);
            return stream.read();
        }
        // Then Delimiter
        for (;;) {
            next = stream.read();
            if (next == -1 || syms[next] != SymbolType.MULTI_SYMBOL_DELIMITER) {
                addToken(token, TerminalType.PUNCTUATOR, row, col);
                return next;
            }
            token += ((char) next);
        }
    }

    private int getConstChar(int next, CharacterStream stream) throws IOException, Exception {
        int separator = next;
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        next = stream.read();
        if (next == '\\') {
            token += ((char) next);
            next = stream.read();
        }
        if (next == -1) {
            throw new Exception("Unexpected end of line.");
        }
        token += ((char) next);
        next = stream.read();
        if (next != separator) {
            throw new Exception("'" + (char) separator + "' expected.");
        }
        token += ((char) next);
        addToken(token, TerminalType.CHARACTER_LITERAL, row, col);
        return stream.read();
    }

    private int getConstString(int next, CharacterStream stream) throws IOException, Exception {
        int separator = next;
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        for (;;) {
            next = stream.read();
            if (next == 13 || next == -1) {
                throw new Exception("Unexpected end of line.");
            }
            token += ((char) next);
            if (next == separator) {
                break;
            }
        }
        addToken(token, TerminalType.STRING_LITERAL, row, col);
        return stream.read();
    }

    private int getIdentifier(int next, CharacterStream stream) throws IOException, Exception {
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        for (;;) {
            next = stream.read();
            while (next == '\\') {
                next = stream.read();
                if (next == -1) {
                    throw new Exception("Unexpected end of line.");
                }
                token += ((char) next);
                next = stream.read();
            }
            if (next == -1 || (syms[next] != SymbolType.LETTER && syms[next] != SymbolType.NUMBER)) {
                addToken(token, TerminalType.IDENTIFIER, row, col);
                return next;
            }
            token += ((char) next);
        }
    }

    private int getConstNumber(int next, CharacterStream stream) throws IOException, Exception {
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        TerminalType tt = TerminalType.INT_LITERAL;
        // Integer part
        for (;;) {
            next = stream.read();
            if (next == -1 || syms[next] != SymbolType.NUMBER) {
                break;
            }
            token += ((char) next);
        }
        // Point
        if (next == '.') {
            // Fractional part
            tt = TerminalType.REAL_LITERAL;
            for (;;) {
                token += ((char) next);
                next = stream.read();
                if (next == -1 || syms[next] != SymbolType.NUMBER) {
                    break;
                }
            }
        }
        // Exponent part
        if (next == 'e' || next == 'E') {
            tt = TerminalType.REAL_LITERAL;
            token += ((char) next);
            next = stream.read();
            if (next == '+' || next == '-') {
                token += ((char) next);
                next = stream.read();
            }
            if (next == -1) {
                throw new Exception("Unexpected end of line.");
            }
            if (syms[next] != SymbolType.NUMBER) {
                throw new Exception("Digit expected.");
            }
            for (;;) {
                token += ((char) next);
                next = stream.read();
                if (next == -1 || syms[next] != SymbolType.NUMBER) {
                    break;
                }
            }
        }
        addToken(token, tt, row, col);
        return next;
    }

    public String getOutput() {
        return output;
    }

    private int getComment(int next, CharacterStream stream) throws IOException, Exception {
        String token = String.valueOf((char) next);
        int col = stream.getCol();
        int row = stream.getRow();
        next = stream.read();
        if (next == -1 || next != commChars.begSecond) {
            // Single symbol delimiter
            if (next == -1 || syms[next] != SymbolType.MULTI_SYMBOL_DELIMITER) {
                addToken(token, TerminalType.PUNCTUATOR, row, col);
                return next;
            }
            // Multiple symbol delimiter
            token += ((char) next);
            for (;;) {
                next = stream.read();
                if (next == -1 || syms[next] != SymbolType.MULTI_SYMBOL_DELIMITER) {
                    addToken(token, TerminalType.PUNCTUATOR, row, col);
                    return next;
                }
                token += ((char) next);
            }
        }
        do {
            next = stream.read();
            if (next == -1) {
                throw new Exception("Unclosed commentary: ");
            }
            if (next == commChars.endFirst) {
                next = stream.read();
                if (next == -1) {
                    throw new Exception("Unclosed commentary: ");
                } else if (next == commChars.endSecond) {
                    return stream.read();
                }

            }
        } while (true);
    }
}
/**
 *
 * @author Maxim
 */

/*
 * public class Program { /** @param args the command line arguments
 */
/*
 * public static void main(String[] args) { Scanner scanner = new Scanner();
 * Queue<Token> tokens = scanner.Parse("test.txt", false); HashMap<Integer,
 * String> ids = scanner.GetIdNames(); for (Token token : tokens) {
 * System.out.format("%5d %5d %20s %s\n", token.row, token.col,
 * token.type.toString(), ids.get(token.id)); }
 * System.out.println(scanner.getOutput()); } }
 */
